﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Devonline.Core;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Devonline.Identity
{
    /// <summary>
    /// Creates a new instance of a persistence store for the specified user type.
    /// </summary>
    /// <typeparam name="TUser">The type representing a user.</typeparam>
    public class ResourceAccessStore : ResourceAccessStore<User, Resource, Level, AccessRule, DbContext, string>
    {
        /// <summary>
        /// Constructs a new instance of <see cref="UserStore{TUser}"/>.
        /// </summary>
        /// <param name="context">The <see cref="DbContext"/>.</param>
        /// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
        public ResourceAccessStore(DbContext context, IdentityErrorDescriber describer = null) : base(context, describer) { }
    }

    /// <summary>
    /// Represents a new instance of a persistence store for the specified user and role types.
    /// </summary>
    /// <typeparam name="TUser">The type representing a user.</typeparam>
    /// <typeparam name="TRole">The type representing a role.</typeparam>
    /// <typeparam name="TContext">The type of the data context class used to access the store.</typeparam>
    /// <typeparam name="TKey">The type of the primary key for a role.</typeparam>
    public class ResourceAccessStore<TUser, TResource, TLevel, TAccessRule, TContext, TKey> :
        IResourceStore<TResource, TKey>,
        ILevelStore<TResource, TKey>,
        IAccessRuleStore<TAccessRule, TKey>,
        IResourceAccessRuleStore<TResource, TKey>,
        IAccessApplyStore<TUser, TResource, TKey>,
        IAccessRecordStore<TUser, TResource, TKey>
        where TKey : IEquatable<TKey>, IConvertible
        where TUser : User<TKey>
        where TResource : Resource<TKey>
        where TLevel : Level<TKey>
        where TAccessRule : AccessRule<TKey>, new()
        where TContext : DbContext
    {
        /// <summary>
        /// Constructs a new instance of <see cref="UserStore{TUser, TRole, TContext, TKey}"/>.
        /// </summary>
        /// <param name="context">The <see cref="DbContext"/>.</param>
        /// <param name="describer">The <see cref="IdentityErrorDescriber"/>.</param>
        public ResourceAccessStore(TContext context, IdentityErrorDescriber describer = null)
        {
            Context = context;
            ErrorDescriber = describer;
        }

        private bool _disposed;
        /// <summary>
        /// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
        /// </summary>
        /// <value>
        /// True if changes should be automatically persisted, otherwise false.
        /// </value>
        public bool AutoSaveChanges { get; set; } = true;
        protected virtual TContext Context { get; }
        protected virtual IdentityErrorDescriber ErrorDescriber { get; }
        protected DbSet<TResource> Resources => Context.Set<TResource>();
        protected DbSet<TAccessRule> AccessRules => Context.Set<TAccessRule>();
        /// <summary>Saves the current store.</summary>
        /// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        protected Task SaveChanges(CancellationToken cancellationToken) => AutoSaveChanges ? Context.SaveChangesAsync(cancellationToken) : Task.CompletedTask;

        #region implement from IIdentityStore
        /// <summary>
        /// find a resource by id
        /// </summary>
        /// <param name="id"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task<TResource> FindByIdAsync(TKey id, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (id.Equals(default))
            {
                throw new ArgumentNullException(nameof(id));
            }

            return Resources.FindAsync(new object[] { id }, cancellationToken).AsTask();
        }
        /// <summary>
        /// find a resource by name
        /// </summary>
        /// <param name="name"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task<TResource> FindByNameAsync(string name, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (name.IsNullOrWhiteSpace())
            {
                throw new ArgumentNullException(nameof(name));
            }

            return Resources.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
        }
        /// <summary>
        /// set resource name
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="name"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetName(TResource resource, string name, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.Name = name;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource alice
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="alias"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetAlias(TResource resource, string alias, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.Alias = alias;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource image
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="image"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetImage(TResource resource, string image, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.Image = image;
            return Task.CompletedTask;
        }
        #endregion

        #region implement from IResourceStore
        /// <summary>
        /// get resources by owner id
        /// </summary>
        /// <param name="ownerId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<IList<TResource>> GetByOwnerAsync(TKey ownerId, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (ownerId.Equals(default))
            {
                throw new ArgumentNullException(nameof(ownerId));
            }

            return await Resources.Where(x => x.OwnerId.Equals(ownerId)).ToListAsync(cancellationToken);
        }
        /// <summary>
        /// get children resources by current resource id
        /// </summary>
        /// <param name="id"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<IList<TResource>> GetChildrenResourcesAsync(TKey id, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (id.Equals(default))
            {
                throw new ArgumentNullException(nameof(id));
            }

            return await Resources.Where(x => x.ParentId.Equals(id)).ToListAsync(cancellationToken);
        }
        /// <summary>
        /// get parent resource of current resource
        /// </summary>
        /// <param name="id"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<TResource> GetParentResourceAsync(TKey id, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (id.Equals(default))
            {
                throw new ArgumentNullException(nameof(id));
            }

            var resource = await FindByIdAsync(id, cancellationToken);
            if (resource == null)
            {
                throw new ArgumentException($"the id of resource {id} not found!");
            }

            return await Resources.FirstOrDefaultAsync(x => x.Id.Equals(resource.ParentId), cancellationToken);
        }
        /// <summary>
        /// get the resources that user has been authorized
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task<IList<TResource>> GetUserAuthorizedResourcesAsync(TKey userId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (userId.Equals(default))
            {
                throw new ArgumentNullException(nameof(userId));
            }

            throw new NotImplementedException();
        }
        /// <summary>
        /// set resource owner
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="ownerId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task SetOwner(TResource resource, TKey ownerId, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.OwnerId = ownerId;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource parent
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="parentId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task SetParent(TResource resource, TKey parentId, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.ParentId = parentId;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource content
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="content"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual Task SetContent(TResource resource, string content, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.Content = content;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource type
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="resourceType"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetResourceType(TResource resource, ResourceType resourceType, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.ResourceType = resourceType;
            return Task.CompletedTask;
        }
        /// <summary>
        /// set resource access level
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="accessLevel"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetAccessLevel(TResource resource, AccessLevel accessLevel, CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            resource.AccessLevel = accessLevel;
            return Task.CompletedTask;
        }
        /// <summary>
        /// 设置资源级别
        /// </summary>
        /// <param name="resource"></param>
        /// <param name="level"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task SetLevel(TResource resource, string level, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (resource == null)
            {
                throw new ArgumentNullException(nameof(resource));
            }

            var existLevel = await Context.Set<TLevel>().FirstOrDefaultAsync(x => x.Name == level, cancellationToken);
            if (existLevel == null)
            {
                throw new ArgumentNullException(nameof(level));
            }

            resource.LevelId = existLevel.Id;
        }
        #endregion

        #region implement from IAccessRuleStore
        /// <summary>
        /// 获取当前资源的所有访问规则
        /// </summary>
        /// <param name="resourceId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<IList<TAccessRule>> GetByResourceIdAsync(TKey resourceId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (resourceId.Equals(default))
            {
                throw new ArgumentNullException(nameof(resourceId));
            }

            return await AccessRules.Where(x => resourceId.Equals(x.ResourceId)).ToListAsync(cancellationToken);
        }
        /// <summary>
        /// 按授权对象获取所有访问规则列表
        /// </summary>
        /// <param name="assignId"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task<IList<TAccessRule>> GetByAssignIdAsync(TKey assignId, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (assignId.Equals(default))
            {
                throw new ArgumentNullException(nameof(assignId));
            }

            return await AccessRules.Where(x => assignId.Equals(x.AssignId)).ToListAsync(cancellationToken);
        }
        /// <summary>
        /// 允许一个访问规则
        /// </summary>
        /// <param name="accessRule"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task Allow(TAccessRule accessRule, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (accessRule == null)
            {
                throw new ArgumentNullException(nameof(accessRule));
            }

            accessRule.AllowType = AllowType.Allow;
            return Task.CompletedTask;
        }
        /// <summary>
        /// 禁止一个访问规则
        /// </summary>
        /// <param name="accessRule"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task Forbid(TAccessRule accessRule, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (accessRule == null)
            {
                throw new ArgumentNullException(nameof(accessRule));
            }

            accessRule.AllowType = AllowType.Forbid;
            return Task.CompletedTask;
        }
        /// <summary>
        /// 设置访问规则的优先级
        /// </summary>
        /// <param name="accessRule"></param>
        /// <param name="priority"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetPriority(TAccessRule accessRule, int priority, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (accessRule == null)
            {
                throw new ArgumentNullException(nameof(accessRule));
            }

            accessRule.Priority = priority;
            return Task.CompletedTask;
        }
        /// <summary>
        /// 设置访问规则的额外条件
        /// </summary>
        /// <param name="accessRule"></param>
        /// <param name="condition"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task SetCondition(TAccessRule accessRule, string condition, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();
            if (accessRule == null)
            {
                throw new ArgumentNullException(nameof(accessRule));
            }

            accessRule.Condition = condition;
            return Task.CompletedTask;
        }
        #endregion

        #region implement from IResourceAccessRuleStore
        /// <summary>
        /// 对当前访问对象添加访问资源的权限
        /// </summary>
        /// <typeparam name="TIdentity"></typeparam>
        /// <param name="identity"></param>
        /// <param name="resource"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual async Task<IdentityResult> AddAccessRuleAsync(IIdentity<TKey> identity, TResource resource, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (identity == null || identity.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(identity));
            }

            if (resource == null || resource.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(resource));
            }

            var assignType = identity.GetAssignType();
            if (await AccessRules.AnyAsync(x => x.AssignType == assignType && identity.Id.Equals(x.AssignId) && resource.Id.Equals(x.ResourceId), cancellationToken))
            {
                return IdentityResult.Failed(identity switch
                {
                    User<TKey> => ErrorDescriber.UserAlreadyInAccessRule(identity.Name, resource.Name),
                    Role<TKey> => ErrorDescriber.RoleAlreadyInAccessRule(identity.Name, resource.Name),
                    Group<TKey> => ErrorDescriber.GroupAlreadyInAccessRule(identity.Name, resource.Name),
                    _ => ErrorDescriber.DefaultError()
                });
            }

            var accessRule = new TAccessRule
            {
                ResourceId = resource.Id,
                AssignId = identity.Id,
                AssignType = identity.GetAssignType()
            };

            accessRule.Create();
            accessRule.Update();

            try
            {
                Context.Add(accessRule);
                await SaveChanges(cancellationToken);
            }
            catch (Exception)
            {
                return IdentityResult.Failed(ErrorDescriber.DefaultError());
            }

            return IdentityResult.Success;
        }
        /// <summary>
        /// 对当前访问对象添加访问资源列表的权限
        /// </summary>
        /// <typeparam name="TIdentity"></typeparam>
        /// <param name="identity"></param>
        /// <param name="resources"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual async Task<IdentityResult> AddAccessRulesAsync(IIdentity<TKey> identity, IList<TResource> resources, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (identity == null || identity.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(identity));
            }

            if (resources.IsNullOrEmpty() || resources.Any(x => x.Id.Equals(default)))
            {
                throw new ArgumentNullException(nameof(resources));
            }

            var assignType = identity.GetAssignType();
            var resoourceIds = await AccessRules.Where(x => x.AssignType == assignType && identity.Id.Equals(x.AssignId) && resources.Select(a => a.Id).Contains(x.ResourceId)).Select(x => x.ResourceId).ToListAsync(cancellationToken);
            if (resoourceIds.IsNotNullOrEmpty())
            {
                resources = resources.Where(x => !resoourceIds.Contains(x.Id)).ToList();
            }

            if (resources.IsNotNullOrEmpty())
            {
                try
                {
                    foreach (var resource in resources)
                    {
                        var accessRule = new TAccessRule
                        {
                            ResourceId = resource.Id,
                            AssignType = assignType,
                            AssignId = identity.Id
                        };

                        accessRule.Create();
                        accessRule.Update();

                        Context.Add(accessRule);
                    }

                    await SaveChanges(cancellationToken);
                }
                catch (Exception)
                {
                    return IdentityResult.Failed(ErrorDescriber.DefaultError());
                }
            }

            return IdentityResult.Success;
        }
        /// <summary>
        /// 对当前访问对象移除资源访问的权限
        /// </summary>
        /// <typeparam name="TIdentity"></typeparam>
        /// <param name="identity"></param>
        /// <param name="resource"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual async Task<IdentityResult> RemoveAccessRuleAsync(IIdentity<TKey> identity, TResource resource, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (identity == null || identity.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(identity));
            }

            if (resource == null || resource.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(resource));
            }

            var assignType = identity.GetAssignType();
            var accessRule = await AccessRules.FirstOrDefaultAsync(x => x.AssignType == assignType && identity.Id.Equals(x.AssignId) && resource.Id.Equals(x.ResourceId), cancellationToken);
            if (accessRule == null)
            {
                return IdentityResult.Failed(identity switch
                {
                    User<TKey> => ErrorDescriber.UserNotInAccessRule(identity.Name, resource.Name),
                    Role<TKey> => ErrorDescriber.RoleNotInAccessRule(identity.Name, resource.Name),
                    Group<TKey> => ErrorDescriber.GroupNotInAccessRule(identity.Name, resource.Name),
                    _ => ErrorDescriber.DefaultError()
                });
            }

            try
            {
                Context.Remove(accessRule);
                await SaveChanges(cancellationToken);
            }
            catch (DbUpdateConcurrencyException)
            {
                return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
            }

            return IdentityResult.Success;
        }
        /// <summary>
        /// 对当前访问对象移除列表中资源访问的权限
        /// </summary>
        /// <typeparam name="TIdentity"></typeparam>
        /// <param name="identity"></param>
        /// <param name="resources"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public virtual async Task<IdentityResult> RemoveAccessRulesAsync(IIdentity<TKey> identity, IList<TResource> resources, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ThrowIfDisposed();

            if (identity == null || identity.Id.Equals(default))
            {
                throw new ArgumentNullException(nameof(identity));
            }

            if (resources.IsNullOrEmpty() || resources.Any(x => x.Id.Equals(default)))
            {
                throw new ArgumentNullException(nameof(resources));
            }

            var assignType = identity.GetAssignType();
            var accessRules = await AccessRules.Where(x => x.AssignType == assignType && identity.Id.Equals(x.AssignId) && resources.Select(a => a.Id).Contains(x.ResourceId)).ToListAsync(cancellationToken);
            if (accessRules.IsNotNullOrEmpty())
            {
                AccessRules.RemoveRange(accessRules);

                try
                {
                    await SaveChanges(cancellationToken);
                }
                catch (DbUpdateConcurrencyException)
                {
                    return IdentityResult.Failed(ErrorDescriber.ConcurrencyFailure());
                }
            }

            return IdentityResult.Success;
        }
        #endregion

        /// <summary>
        /// TODO
        /// </summary>
        /// <param name="user"></param>
        /// <param name="resource"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task Apply(TUser user, TResource resource, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
        /// <summary>
        /// TODO
        /// </summary>
        /// <param name="user"></param>
        /// <param name="resource"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public Task Record(TUser user, TResource resource, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Throws if this class has been disposed.
        /// </summary>
        protected void ThrowIfDisposed()
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(GetType().Name);
            }
        }
        /// <summary>
        /// Dispose the stores
        /// </summary>
        public void Dispose()
        {
            _disposed = true;
            GC.SuppressFinalize(this);
        }
    }
}
